package com.google.code.wicket.auth.component.strategy;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.authorization.Action;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.authorization.IUnauthorizedComponentInstantiationListener;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.settings.ISecuritySettings;

import com.google.code.wicket.auth.component.marker.Marker;
import com.google.code.wicket.auth.component.strategy.part.DynamicAuthorizationStrategy;
import com.google.code.wicket.auth.component.strategy.part.StaticAuthorizationStrategy;
import com.google.code.wicket.auth.component.value.provider.ValueProvider;
import com.google.code.wicket.auth.component.value.resolver.ValueResolver;

/**
 * An {@link IAuthorizationStrategy} based on several strategies to look up
 * required and available privileges in a generic way.
 * <p>
 * Per default, this will install a
 * {@link MarkerBasedUnauthorizedComponentInstatiationListener} as
 * {@link IUnauthorizedComponentInstantiationListener}. If you turn this off,
 * you manually need to deal with your components that are marked as invalid.
 * <p>
 * You can use this strategy alone, or as a static or dynamic part of a
 * {@link ComposedAuthorizationStrategy}, as it implements both
 * {@link DynamicAuthorizationStrategy} and {@link StaticAuthorizationStrategy}.
 * 
 * @param <T>
 *            the privilege type (e.g. string or a custom enum)
 */
public class ComponentClassAuthorizationStrategy<T> implements
        IAuthorizationStrategy, StaticAuthorizationStrategy,
        DynamicAuthorizationStrategy {

    private final ValueResolver<T> resolver;

    private final ValueProvider<T> provider;

    private final Marker<T> componentMarker;

    private final Class<? extends Page>[] ignoredPageClasses;

    private Action actionType;

    private boolean checkBookmarkableLinks = true;

    /**
     * Constructor with delegates to lookup required and available privileges, a
     * strategy to deal with missing privileges and an optional array of ignored
     * pages.
     * 
     * @param resolver
     *            what privileges are needed?
     * @param provider
     *            what privileges are available?
     * @param componentMarker
     *            what will happen to unauthorized components?
     * @param actionType
     *            an {@link Action} type.
     * @param installUnauthorizedComponentListener
     *            if true, install a
     *            {@link MarkerBasedUnauthorizedComponentInstatiationListener}
     *            (highly recommended, default in other constructors)
     * @param ignoredPageClasses
     *            optional page classes to ignore (components on these pages
     *            will be ignored)
     */
    public ComponentClassAuthorizationStrategy(final ValueResolver<T> resolver,
            final ValueProvider<T> provider, final Marker<T> componentMarker,
            final Action actionType,
            final boolean installUnauthorizedComponentListener,
            final Class<? extends Page>[] ignoredPageClasses) {
        this.resolver = resolver;
        this.provider = provider;
        this.componentMarker = componentMarker;
        this.ignoredPageClasses = ignoredPageClasses;
        this.actionType = actionType;
        if (installUnauthorizedComponentListener) {
            this.doInstallUnauthorizedComponentListener();
        }
    }

    /**
     * Constructor with delegates to lookup required and available privileges, a
     * strategy to deal with missing privileges and an optional array of ignored
     * pages.
     * 
     * @param resolver
     *            what privileges are needed?
     * @param provider
     *            what privileges are available?
     * @param componentMarker
     *            what will happen to unauthorized components?
     * @param actionType
     *            an {@link Action} type.
     * @param ignoredPageClasses
     *            optional page classes to ignore (components on these pages
     *            will be ignored)
     */
    public ComponentClassAuthorizationStrategy(final ValueResolver<T> resolver, //
            final ValueProvider<T> provider, //
            final Marker<T> componentMarker, //
            final Action actionType, //
            final Class<? extends Page>... ignoredPageClasses //
    ) {
        this(resolver, provider, componentMarker, actionType, true,
                ignoredPageClasses);

    }

    /**
     * Constructor with delegates to lookup required and available privileges, a
     * strategy to deal with missing privileges and an optional array of ignored
     * pages.
     * <p>
     * Will use {@link Action#RENDER} as action type.
     * 
     * @param resolver
     *            what privileges are needed?
     * @param provider
     *            what privileges are available?
     * @param componentMarker
     *            what will happen to unauthorized components?
     * @param ignoredPageClasses
     *            optional page classes to ignore (components on these pages
     *            will be ignored)
     */
    public ComponentClassAuthorizationStrategy(final ValueResolver<T> resolver, //
            final ValueProvider<T> provider, //
            final Marker<T> componentMarker, //
            final Class<? extends Page>... ignoredPageClasses //
    ) {
        this(resolver, provider, componentMarker, Component.RENDER,
                ignoredPageClasses);

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isActionAuthorized(final Component component,
            final Action action) {
        boolean result;

        if (action.equals(this.actionType)
                || this.isIgnoredClass(component.getPage().getClass())) {
            result = true;
        } else {

            if (this.resolver.useSingleValues()) {
                result = this.checkSingleValue(component);
            } else {
                result = this.checkMultiValues(component);

            }
            if (result && this.checkBookmarkableLinks
                    && component instanceof BookmarkablePageLink<?>) {
                result = Application.get().getSecuritySettings()
                        .getAuthorizationStrategy().isInstantiationAuthorized(
                                ((BookmarkablePageLink<?>) component)
                                        .getPageClass());
            }

        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <X extends Component> boolean isInstantiationAuthorized(
            final Class<X> componentClass) {
        Collection<T> coll;
        if (this.resolver.useSingleValues()) {
            coll = this.getCollectionFromSingleValue(componentClass);

        } else {
            coll = this.resolver.getValues(componentClass);
        }
        return this.areAllValuesAvailable(coll, componentClass, null);
    }

    /**
     * Per default, permissions for the target class of a
     * {@link BookmarkablePageLink} are also checked. Call this method with
     * value false to turn this behavior off.
     * 
     * @param checkBookmarkableLinks
     *            the new value to set
     * @return self reference for chaining
     */
    public ComponentClassAuthorizationStrategy<T> setCheckBookmarkableLinks(
            final boolean checkBookmarkableLinks) {
        this.checkBookmarkableLinks = checkBookmarkableLinks;
        return this;
    }

    private boolean areAllValuesAvailable(final Collection<T> requiredValues,
            final Class<? extends Component> componentClass,
            final Component component) {
        boolean result;
        final Set<T> missingValues = new HashSet<T>(requiredValues);
        if (this.provider.useSingle()) {
            final Iterator<T> it = missingValues.iterator();
            while (it.hasNext()) {
                final T value = it.next();
                if (this.provider.hasValue(value)) {
                    it.remove();
                }

            }
        } else {
            missingValues.removeAll(this.provider.getValues());
        }
        result = missingValues.isEmpty();
        if (!result) {
            this.componentMarker.mark(component, missingValues);
        }
        return result;
    }

    private boolean checkMultiValues(final Component component) {
        final boolean result;

        final Class<? extends Component> componentClass = component.getClass();
        if (this.isIgnoredPageClass(componentClass)) {
            result = true;
        } else {
            final Collection<T> requiredValues = this.resolver
                    .getValues(componentClass);
            result = requiredValues.isEmpty() ? true : //
                    this.areAllValuesAvailable(//
                            requiredValues, component.getClass(), component);
        }
        return result;

    }

    private boolean checkSingleValue(final Component component) {
        final Class<? extends Component> componentClass = component.getClass();
        final Collection<T> coll = this
                .getCollectionFromSingleValue(componentClass);
        return this.areAllValuesAvailable(coll, componentClass, component);
    }

    private void doInstallUnauthorizedComponentListener() {
        final ISecuritySettings securitySettings = Application.get()
                .getSecuritySettings();
        final IUnauthorizedComponentInstantiationListener existingListener = securitySettings
                .getUnauthorizedComponentInstantiationListener();
        final IUnauthorizedComponentInstantiationListener newListener = existingListener == null ? new MarkerBasedUnauthorizedComponentInstatiationListener<T>(
                this.componentMarker)
                : new MarkerBasedUnauthorizedComponentInstatiationListener<T>(
                        existingListener, this.componentMarker);
        securitySettings
                .setUnauthorizedComponentInstantiationListener(newListener);
    }

    private Collection<T> getCollectionFromSingleValue(
            final Class<? extends Component> componentClass) {
        final T value = this.isIgnoredPageClass(componentClass) ? null
                : this.resolver.getSingleValue(componentClass);
        final Collection<T> coll = value == null ? Collections.<T> emptySet()
                : Collections.<T> singleton(value);
        return coll;
    }

    private boolean isIgnoredClass(final Class<? extends Page> pageClass) {
        boolean result = false;
        for (final Class<? extends Page> ignoredPageClass : this.ignoredPageClasses) {
            if (pageClass.isAssignableFrom(ignoredPageClass)) {
                result = true;
                break;
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private boolean isIgnoredPageClass(
            final Class<? extends Component> componentClass) {
        return componentClass.isAssignableFrom(Page.class)
                && this.isIgnoredClass((Class<? extends Page>) componentClass);
    }

}
